refactor(types)!: rename the class-name/style zod object outright, no deprecated alias - #7577
Conversation
`StylePropsSchema` (2 keys: `className`, `style`) and the TypeScript `StyleProps` (the Tailwind-scale vocabulary: `padding`, `margin`, `gap`, …) shared a name and zero keys, in a package where the `…Schema` suffix otherwise means "runtime mirror of the like-named declaration" (154 registered pairs, counted from the AST of `MIRRORS` in `zod-mirror-parity.test.ts`). The false pair is what a name-derived pairing produced when objectui#5684's registry was built. - rename the zod const to `ClassNameStylePropsSchema`; keep `StylePropsSchema` as a deprecated alias of the SAME object for one release, exported from the published `@object-ui/types/zod` barrel, so the rename does not narrow the surface; - teach the name-pairing to skip explicit non-pairs WITH A STATED REASON (`NAME_NON_PAIRS`), and re-measure the claim each reason rests on every run: the named declaration must exist and the two sides must share no key, so a collision that becomes PARTIAL turns red instead of comparing like a mirror. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…ring A written count in this header is prose that rots — objectui#6141 measured exactly that on the three counts already there. The skip list names the population as `Object.keys(MIRRORS)` instead of quoting a number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
CI red —
|
| query | result |
|---|---|
StylePropsSchema in packages/plugin-detail, packages/react, packages/components |
0 |
lit control BaseSchema, same query shape, same roots |
30 files |
The zero is a measurement, not a query that cannot reach. Nothing this diff renames is reachable from the failing test.
Corroborating, same base (ac8abb0f5), same shard, same farm, within the same half hour: #7573, #7574 and #7576 all ran shard 2/4 green.
⛔ "Flake" is not the root cause — the root cause is nameable
RelatedList.serverpagination.test.tsx:114-119:
fireEvent.click(nextButton());
await screen.findByText('Page 3 of 3'); // gates on the LABEL only
expect(h.schema.data).toHaveLength(2); // reads the REFETCHED dataThe same test gates the chain one step earlier and then stops doing it — its page-2 step waits on the refetch, its page-3 step does not. The label is driven by pagination state and commits before the refetched rows reach h.schema.data, so the assertion samples the previous page. The received 5 is the page size, which is why this is a race and not an arithmetic error.
This is #7075's family. PR #7076 repaired the four files that card named; serverpagination is not among them, because #7075's census probed the schema chain (deferring getObjectSchema by 50ms) and this site rides the pagination refetch chain. That probe could not have seen it.
Filed as #7579, with the repair shape and — importantly — the reason the obvious patch is wrong: a waitFor(() => expect(ds.find).toHaveBeenCalledWith(…, { $skip: 10 })) gate settles the call, not the value, which is the trap #7075 and #6959 already recorded. It would make this site flake less and fail identically.
What I am doing, and not doing
- ⛔ Not fixing it here. The failing code is unrelated to this change and no fix for it exists yet, so the rule is to say what is failing with a proposed patch rather than widen the PR — and this is a
Clause-②: yesPR awaiting contract review, which is the worst possible place to bolt on an unrelated test repair. - ✅ Re-running the failed job once. That re-run is now spent; a second failure is real and I will treat it as this PR's.
⚠️ Separately, this PR is blocked on something else
The isolated contract review this PR requires (Clause-②: yes, needs:contract-review) terminated early on a model rate limit and produced no verdict. A partial review is not a verdict and will not be treated as one — it is adopted whole or not at all. This PR does not land until a complete review exists, regardless of what CI does. Recorded here so the state is not misread as "green and waiting".
Generated by Claude Code
|
CI is green on the re-run: 32 checks, 29 success, 3 skipped, zero failures, zero still running. ⛔ This PR still does not land. The blocker is unchanged and is not CI: the isolated contract review this Held green, awaiting a complete review. Not queued. Generated by Claude Code |
|
Contract review verdict for this head ( Generated by Claude Code |
⛔ HELD INDEFINITELY on the
|
| head | 681053908, unchanged since 00:00Z |
| merge-base | ac8abb0f5 |
| main | e30ed155c (has advanced 4 merges since the last check) |
| conflict with current main | none (git merge-tree) |
| CI | green — 29 success / 3 skipped / 0 failed |
| merge queue | eligible, deliberately unqueued |
It is blocked on one thing: this change is Clause-②: yes, which routes it through the CONTRACT_REVIEW_TIER (claude-fable-5) before it may land, and that tier has refused four times:
~00:05Z 429 ~00:33Z 429 ~01:37Z 429 ~03:5xZ 429
Roughly four hours, with the interval widened after the third failure because three misses inside 90 minutes indicated the quota window is longer than an hour. The fourth attempt was a fresh isolated reviewer, fed only #5928, this PR and its diff, fenced to the merge-base and given no conclusion of mine. It terminated on the limit before producing a verdict.
⛔ What I did not do
- Did not substitute a different model tier. Landing a
Clause-②: yeschange on a review from the wrong tier is exactly what this gate exists to prevent, and a cheaper reviewer's PASS would be worse than no PASS — it would look like the gate had been satisfied. - Did not accept a partial review as a verdict. The reviewer was instructed to say plainly if it could not finish and to emit no verdict line rather than guess. It did that correctly.
- Did not decide the disposition myself. Four failures across four hours is a resource question, not a technical one.
The decision
Put to the owner as three options — keep waiting on a longer interval, add usage credits and retry immediately, or hold indefinitely and move on. The owner chose: hold indefinitely.
⇒ The retry check-in is deleted; this PR will not be retried on a schedule. It stays open, green and unqueued until someone reopens the question.
⚠️ This is not one card
The same gate holds at least three others in this lane, all Clause-②: yes and all otherwise ready:
| card | state |
|---|---|
| #6033 | ruled 2026-08-25 with a complete per-key table; the ruling itself specifies "contract-review tier at dispatch" |
| #6497 | blocker discharged, island measurement re-verified on the merged ref; retirement narrows a published surface |
| #7479 | option C adopted 2026-09-03; its own body predicts Clause-② yes |
⇒ The quota is not blocking a PR. It is blocking a class of work — every published-surface change this lane is otherwise ready to land. Recording that here because the cost is invisible if it is only ever noted one card at a time.
To resume
Add Fable usage credits, then re-run an isolated contract review against merge-base ac8abb0f5 (⛔ not current main — this repo takes ~18 merges/day and diffing a behind branch against main renders other people's commits inverted). Adopt the verdict verbatim or void it entirely; ⛔ never edit, abridge or polish it. On PASS, flip ready and enqueue, ⛔ confirming enqueue by git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-7577-*' and never by the tool's own notice — tonight that notice reported method: MERGE for calls made with SQUASH, and blank for others.
Generated by Claude Code
|
Director seat (summon 13, session_01WXyGTWPbbreqXow7Z2pZCk) to the The verdict is REWORK, not a missing review: the rename passes; the one-release deprecated alias does not, under the standing 2026-08-27 ruling (retirements and deprecated aliases retire immediately unless a named external consumer justifies a window; none is named). The ask is on the card: drop the alias and its barrel line, remove On the three other cards named (#6033, #6497, #7479): when their PRs exist, hang the carrier on PR and card as usual; the director seat reviews Generated by Claude Code |
⛔ Retracting my "held indefinitely" note above — its premise was stale, and the error is mine
A complete contract review at tier already existed when I wrote that. The director seat delivered it at 01:32Z ( I posted "no verdict exists, held indefinitely on the quota" at 03:49Z — two hours and seventeen minutes after the verdict landed. How I got it wrong, preciselyThe check-in I armed at 01:37Z carried the sentence "THREE attempts, all terminated on a 429 with no verdict." Before declaring the hold I verified four things by git: head, main, conflict state, CI. ⛔ I did not read this PR's comment thread. The verdict was sitting in it. ⭐ That is the exact failure I identified, wrote a rule against, and put into my own handoff tonight: "no card is classified from its body alone — read the thread tail and treat it as authoritative where they disagree." I applied it to issue cards, found two dispatchable cards by doing so, and then did not apply it to a pull request. Fourth instance in one session, and the first that cost a decision: the hold/wait/pay question I escalated was answered on information I had not checked.
What is actually happening nowThe REWORK is dispatched, with the verdict reproduced verbatim to the implementer and marked not-reinterpretable. The patch round carries exactly what §③ asks: drop ⛔ The implementer is instructed not to enqueue and not to flip ready — carriers stay on, and the director seat reviews the patch round, not me. Generated by Claude Code |
… alias The deprecated `StylePropsSchema` alias and its barrel line are removed: `ClassNameStylePropsSchema` is the only spelling on `@object-ui/types/zod`. Maintainer ruling, 2026-08-27: a deprecated alias needs named external-consumer evidence, and this rename has none — no window, no second spelling. `NAME_NON_PAIRS` existed only to skip that alias, so it goes with it. With no like-named pair left in the tree the const is accounted for through the mechanism `zod-mirror-parity.test.ts` already has for a const with no TypeScript twin: an `EXCLUSIONS` entry with a stated reason. Both pins stay, renamed for what they now prove: the published barrel exports the new name as a live schema and no longer carries the retired one, and the object carries exactly the two keys its name claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
The inherited sentence claimed a `tsc` run stays green when the name never reaches the barrel. Measured on this branch by deleting the barrel line: the suite fails at module load AND `tsc -p tsconfig.test.json` fails with TS2305 — both because this file names the export. The honest claim is narrower and is the one that justifies the file: nothing else in the package imports the barrel's copy, so without this import the barrel line is load-bearing for nobody. The value-import half is stated separately, since that is what a type import could not do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
The 199-file lit control was inherited from the previous round and does not reproduce under either query shape run here (251 files word-boundary, 252 substring). Numbers that get quoted onward are the failure this package keeps paying for, so the changeset now carries the reading taken on this tree, with the method beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Patch round pushed and green — awaiting the director seat's review
Head CI on that head: 29 success · 3 skipped · 0 failed · nothing running. The dev's own report snapshotted it mid-flight at 04:19Z with 8 jobs still in progress; this is the converged reading, recorded so the review does not have to re-derive it. The two structural asks of §③, verified by me on the branch rather than taken from the report:
⭐ Two things from the round worth having in front of the review rather than buried in the report:
One judgement I made so it is not re-asked: the dev declined to edit card #5928's body to record the terminal state, because that body carries a tag-shaped generic fragment and this repo has six measured cases of GitHub eating those on save. I agree — leave the body alone. "renamed, no alias" is recorded in its card comment, which is sufficient. Over to you. Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Enqueueing — recording exactly what I am acting on, because the release carried no comment
Enqueueing with squash now. ⛔ Landing will be confirmed by git — parent count on Generated by Claude Code |
|
|
Fixes #5928
Clause-②: yes — unchanged from the contract review that set it. The diff still moves a name on a published surface (
@object-ui/types/zod), and it now also REMOVES the old one, so the flag is if anything stronger than when it was determined.needs:contract-reviewstays on this PR and on the card.Patch round (2026-09-04) — the REWORK verdict, adopted
The director seat's verdict was REWORK on one point: the deprecated alias. Standing maintainer ruling of 2026-08-27 — a deprecated alias or a staged retirement is recommendable only on NAMED external-consumer evidence; this rename has none, so there is no window and no second spelling. Renamed outright.
Removed in this round
export const StylePropsSchema = ClassNameStylePropsSchemainzod/base.zod.ts, and its line in the published barrelzod/index.zod.ts.NAME_NON_PAIRSinzod-mirror-parity.test.ts, entirely — it existed only to skip the alias, and with the alias gone there is no like-named pair left in the tree. Its AST reader (declaredMemberNames), its seven-test suite and its census wiring go with it; that file's imports return to their pre-PR spelling.The diff against the merge-base
ac8abb0f5is now 5 files, +125 / −9 (it was +242 / −9).The const is registered through the path that already existed
zod-mirror-parity.test.tsalready carries one mechanism for a const with no TypeScript twin, and that is the one used — no second mechanism is introduced. Its own docstring states the contract:So the registration is a key rename inside that map, and that is now the entire parity-test diff against the merge-base:
Two assertions that were already in the file keep the entry honest, with nothing new added:
every exclusion carries a reason(a reason shorter than 20 characters fails), and the censusevery exported const in ../zod/ is either a registered pair or an excluded one.The two pins, renamed for what they now prove
style-props-alias-5928.test.tsis renamed toclassname-style-props-rename-5928.test.ts: with no alias, the old filename asserted the existence of a thing that is gone — the defect class this card is about.the published barrel exports it as a live schema — and no longer carries the retired name(was two tests: "the deprecated alias is the SAME object" and "the alias is a live schema"). It imports the new name through the published barrel path, parses a good value, takes a refusal addressed to theclassNamepath, and then reads the barrel's own export list to assertStylePropsSchemais not on it under any spelling. Positive control on the same object in the same run, so the absence measures the removal and not a broken import.it carries exactly the two keys its name claims(same assertion, renamed off the alias). A third key makes the name a lie and has to be a decision.Division of labour, measured rather than assumed. The census reads
export constdeclarations, so a returning DEFINITION reddens it with no help from the pin file; what it cannot see is the same name returning as a re-export alias. That is why the absence is read off the barrel object. Both halves are ablated below (legs A1 and A2).Ablation legs
Every leg mutates a FACT, never an assertion. Each ran under
trap ... EXIT INT TERMwith absolute paths; the mutation was proven on disk before any result was read (anchored marker count plus agit hash-objectdiffering from the HEAD blob, with an empty hash read as FAILURE); the restore was proven by state —git diff HEADempty for the path AND the on-disk blob equal to the HEAD blob, both non-empty — viagit checkout HEAD -- ABSOLUTE_PATH, never the bare form. The implementation was committed first, so HEAD was a restore point that contained it.export const StylePropsSchema = ClassNameStylePropsSchemainzod/base.zod.tsevery exported const in ../zod/ is either a registered pair or an excluded oneFAILS. The pin file stays green: the const never reached the barrel.export braces ClassNameStylePropsSchema as StylePropsSchema)`StylePropsSchema` is back on the published ./zod surface — the rename was outright, no alias: expected true to be false. The census stays green — exactly the gap this pin covers.EXCLUSIONSentry for the renamed const is deletedtsc -p tsconfig.test.jsonexit 2 withTS2305: Module '"../zod/index.zod.js"' has no exported member 'ClassNameStylePropsSchema'.it carries exactly the two keys its name claims:expected [ 'ablationThirdKey', …(2) ] to deeply equal [ 'className', 'style' ].Leg C corrected a claim inherited from the previous round. That docstring said a
tscrun stays green when the name never reaches the barrel; it does not, because this file's own import is what both checkers trip over. The docstring now states the narrower thing that is true and was measured: nothing else in the package imports the barrel's copy of this name, so without this file the barrel line is load-bearing for nobody — and the import is a VALUE import because a type import erases the halfsafeParseproves.Changeset —
@object-ui/typesminorMinor, on this repository's own convention for retirements:
scripts/check-changeset-no-major.mjsfails anymajor(amajorin this fixed group would push all 39 packages off objectstack's major), and objectui#7487 shipped a retirement as minor with the breaking marker in the title. The breaking half is carried by the title marker and stated plainly in the changeset body — the old name is gone, import the new one.Verification — union re-run at the final commit
306924df4git rev-parse --short HEADfrom that run:306924df4. Worktree clean, every leg below run at that commit. Exit codes captured by redirect-then-read, never through a pipe; every verdict quoted is the line the gate printed. Vitest was run from the repo root with path filters — the package-cwd form is refused byscripts/vitest-invocation-guard.mjsas a known false green.pnpm exec vitest run packages/types/(repo root)Test Files 102 passed (102)/Tests 1728 passed (1728)pnpm --filter @object-ui/types run type-check(all three tsc projects)pnpm --filter @object-ui/types run lint269 problems (0 errors, 269 warnings)— 0 errors; the 269 warnings are pre-existing and unchangednode scripts/check-changeset-presence.mjs4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.node scripts/check-control-bytes.mjsOK (scanned 6240 tracked text file(s); skipped 85 binary)pnpm check:self-importNo package names itself inside its own src/.pnpm check:spec-symbolspnpm check:esm-specifiersno un-ledgered package emits an extensionless relative specifierpnpm check:vi-mock-specifiers,pnpm check:vi-mock-inheritnode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errorsnode scripts/check-type-check-coverage.mjs41/41 packages compile their testsThe test count moved 1736 to 1728, by exactly the arithmetic of the removals: the seven-test
NAME_NON_PAIRSsuite, plus one of the three tests in the renamed pin file (the alias-identity test, which now has no subject). 102 test files before and after — the pin file was renamed, not dropped.Two things that could have been vacuous, checked rather than assumed:
type-checkreally does compile the tests:tsc -p tsconfig.test.json --listFileslists bothclassname-style-props-rename-5928.test.tsandzod-mirror-parity.test.ts(1 hit each), andcheck-type-check-coverage.mjsreports41/41 packages compile their tests.eslint . --format jsoninpackages/typesreports 167 files judged, and all four touched files are in that population with 0 errors.Narrowings, each with what a wider run could have caught:
packages/types/. A repo-wide run could catch a consumer elsewhere. The sweep below measures that no file outsidepackages/typesnames this export at all, and CI runs the full suite regardless.@object-ui/types. Both are that package's whole job, not a file subset.eslint.config.jsconfigures noparserOptions.projectand noprojectService, so linting here is not type-aware and this diff cannot move the verdict of a file it did not touch.check:readme-exports— NOT MEASURED locally, and not read as a failure. It needs a built tree (its type entry ./dist/index.d.ts is not on disk -- run pnpm build first) for 43 packages, most untouched here. That is an unmet prerequisite, not a result. This export appears in no README (measured below). CI builds first and runs it.pnpm check(the CLI aggregate) not run locally — it needs a built@object-ui/cli; it is a CI job.Consumer sweep, re-measured on this branch
git grepover tracked files, word-boundary soClassNameStylePropsSchemacannot mask the old name:Lit control on the same query shape, this tree:
BaseSchemamatches 251 tracked files (252 as a plain substring). The previous round's body quoted 199 for this control; that number does not reproduce under either shape here, so the changeset now carries the reading taken on this tree with its method beside it, rather than inheriting a count — the failure objectui#6141 measured in this very file.What this seat still cannot see, stated rather than concluded away: no view of consumers outside this repository. The in-repo zero says nothing about them. Under the 2026-08-27 ruling that is not a reason for an alias — an alias needs NAMED external-consumer evidence, and none was offered in review.
Deliberately not done
zod-mirror-parity.test.tsare untouched — objectui#7433's subject, awaiting a maintainer. Mechanically checked:git diff ac8abb0f5..HEADfor that file is the six-line exclusion hunk and nothing else, so no line of that header is in this diff..describe('Style properties')is unchanged. It is vague in the same way the name was, but it is runtime metadata on a published schema and the card ruled on the name. A separate, declarable change.content/docs/releases/, and no docs update: the sweep found zero references to this export outsidepackages/types/src.🤖 Generated with Claude Code
Session for this patch round:
session_01EMrWaQw3XS5DxTHxp4yRyCGenerated by Claude Code